package com.dragon.backstage.interceptor;

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.StpUtil;
import com.dragon.common.annotation.IgnoreLogin;
import com.dragon.common.constant.HttpStatus;
import com.dragon.common.utils.AjaxResult;
import com.dragon.framework.context.SecurityContextHolder;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;

/**
 * 后台登陆拦截器
 *
 * @author dragon
 * @version 1.0
 * @date 2024/7/26 16:43
 */
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 404重定向URL
     */
    private static final String ERROR_URL = "/error";

    @Resource
    private ObjectMapper objectMapper;


    /**
     * 用来拦截处理器的执行，preHandle方法将在Controller处理之前调用的。
     * SpringMVC里可以同时存在多个interceptor，它们基于链式方式调用，每个interceptor都根据其声明顺序依次执行。这种链式结构可以中断，当方法返回false时整个请求就结束了
     */
    @Override
    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws IOException {
        log.warn("***** 登陆拦截器 *****");
        String queryString = request.getQueryString();
        log.warn("请求类型 {} 请求URL {}", request.getMethod(), request.getRequestURI() + (StringUtils.isBlank(queryString) ? "" : "?" + queryString));

        if (ERROR_URL.equals(request.getRequestURI())) {
            reject(response, HttpStatus.NOT_FOUND, "请求URL不可用");
            return false;
        }

        // 如果不是映射到方法直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        Method method = ((HandlerMethod) handler).getMethod();
        // 检查是否忽略token校验
        if (method.isAnnotationPresent(IgnoreLogin.class)) {
            return true;
        }

        /*
         * 具体登陆业务逻辑
         */
        try {
            Long userId = StpUtil.getLoginIdAsLong();

            // 登陆用户ID写入安全上下文
            SecurityContextHolder.setUserId(userId);
        } catch (NotLoginException nle) {
            int code = Integer.parseInt(nle.getType());
            if (NotLoginException.NOT_TOKEN.equals(nle.getType())) {
                // 请求中读取不到token
                reject(response, code, "会话不存在，请重新登录");
            } else if (NotLoginException.INVALID_TOKEN.equals(nle.getType())) {
                // token无效
                reject(response, code, "无效的会话，请重新登录");
            }else if (NotLoginException.TOKEN_TIMEOUT.equals(nle.getType())) {
                // token已经过期
                reject(response, code, "会话已过期，请重新登录");
            } else if (NotLoginException.BE_REPLACED.equals(nle.getType())) {
                // token已被顶下线
                reject(response, code, "会话失效，已在其他设备登录");
            } else if (NotLoginException.KICK_OUT.equals(nle.getType())) {
                // token已被踢下线
                reject(response, code, "会话已被踢下线，请重新登录");
            } else {
                // token无效
                reject(response, code, "无效的会话，或者会话已过期，请重新登录");
            }

            return false;
        }

        return true;
    }

    /**
     * postHandle 会在Controller方法调用之后，但是在DispatcherServlet 渲染视图之前调用。因此我们可以在这个阶段，对将要返回给客户端的ModelAndView进行操作
     */
    @Override
    public void postHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    /**
     * afterCompletion在当前interceptor的preHandle方法返回true时才执行。
     * 该方法会在整个请求处理完成后被调用，就是DispatcherServlet渲染视图完成以后，主要是用来进行资源清理工作。需要注意的是，afterCompletion在interceptor链式结构中以相反的顺序执行，也就是说先申明的interceptor返回会后调用。
     */
    @Override
    public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

    /**
     * 返回JSON数据
     *
     * @param response 响应对象
     * @param code     错误码
     * @param message  错误信息
     * @throws IOException IO异常
     */
    private void reject(HttpServletResponse response, int code, String message) throws IOException {
        response.reset();
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter pw = response.getWriter();
        pw.write(objectMapper.writeValueAsString(AjaxResult.failed(code, message)));
    }

}
